package com.lifeonwalden.game.mindustry.world;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.lifeonwalden.game.mindustry.Global;
import com.lifeonwalden.game.mindustry.constant.*;
import com.lifeonwalden.game.mindustry.entity.*;
import com.lifeonwalden.game.mindustry.feature.TextureUnit;
import com.lifeonwalden.game.mindustry.model.RecipeItem;

import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

public class World implements Disposable {
    public Point[][] points;
    public Array<Ground> grounds = new Array<>(false, 16);
    public Array<Surface> surfaces = new Array<>(false, 16);
    public Player player;
    public Base base;
    private short scaleCounter = 20;
    public double scaling = this.scale();
    public ResourceWarehouse resourceWarehouse = new ResourceWarehouse();
    public Array<Bullet> bullets = new Array<>(false, 16);

    /**
     * setup world
     */
    public World(Pixmap pixmap, TextureAtlas textureAtlas) {
        this.points = new Point[(short) pixmap.getWidth() * 8][(short) pixmap.getHeight() * 8];
        bindTextures(textureAtlas);
        setupMap(pixmap);
        resourceWarehouse.stoneCount = 100;
    }

    public void zoomIn() {
        this.scaleCounter--;
        this.scaling = this.scale();
    }

    public void zoomOut() {
        this.scaleCounter++;
        this.scaling = this.scale();
    }

    public void addBullet(Bullet bullet) {
        this.bullets.add(bullet);
    }

    private float scale() {
        return (float) Math.pow(0.9, this.scaleCounter);
    }

    public void doJobBefore() {
        for (Bullet bullet : this.bullets) {
            bullet.doJobBefore();
        }

        Surface surface;
        for (int i = this.surfaces.size - 1; i >= 0; i--) {
            surface = this.surfaces.get(i);
            if (surface.destroyed) {
                surface.destroy();
                this.surfaces.removeIndex(i);
            } else {
                surface.doJobBefore();
            }
        }

        base.doJobBefore();
        player.doJobBefore();
    }

    public void draw(Batch batch) {
        grounds.forEach(ground -> ground.draw(batch));

        for (Surface surface : this.surfaces) {
            if (!surface.destroyed) {
                surface.draw(batch);
            }
        }
        base.draw(batch);
        player.draw(batch);

        for (Bullet bullet : this.bullets) {
            if (!bullet.runOut) {
                bullet.draw(batch);
            }
        }
    }

    public void doJobAfter() {
        Bullet bullet;
        for (int i = this.bullets.size - 1; i >= 0; i--) {
            bullet = this.bullets.get(i);
            if (bullet.runOut) {
                this.bullets.removeIndex(i);
            } else {
                bullet.doJobAfter();
            }
        }

        Surface surface;
        for (int i = this.surfaces.size - 1; i >= 0; i--) {
            surface = this.surfaces.get(i);
            if (surface.destroyed) {
                surface.destroy();
                this.surfaces.removeIndex(i);
            } else {
                surface.doJobAfter();
            }
        }
        base.doJobAfter();
        player.doJobAfter();
    }

    public void removeUnit(Coordinate coordinate) {
        Point point = this.points[coordinate.x][coordinate.y];
        if (null != point) {
            if (null != point.obstruction) {
                if (point.obstruction instanceof Base || point.obstruction instanceof Player) {
                    return;
                }

                point.obstruction.destroyed = true;

                int startX = point.obstruction.x;
                int startY = point.obstruction.y;
                int endX = startX + point.obstruction.type.texture.getRegionWidth() - 1;
                int endY = startY + point.obstruction.type.texture.getRegionHeight() - 1;
                for (int x = startX; x <= endX; x++) {
                    for (int y = startY; y <= endY; y++) {
                        this.points[x][y].obstruction = null;
                    }
                }

            } else if (null != point.surface) {
                point.surface.destroyed = true;

                int startX = point.surface.x;
                int startY = point.surface.y;
                int endX = startX + point.surface.type.texture.getRegionWidth() - 1;
                int endY = startY + point.surface.type.texture.getRegionHeight() - 1;
                for (int x = startX; x <= endX; x++) {
                    for (int y = startY; y <= endY; y++) {
                        this.points[x][y].surface = null;
                    }
                }
            }
        }
    }

    public void removeUnit() {
        removeUnit(projectedCoordinate(Gdx.input.getX(), Gdx.input.getY()));
    }

    public Coordinate projectedCoordinate(int screenX, int screenY) {
        Coordinate coordinate = new Coordinate();
        int centerX = (int) (Gdx.graphics.getWidth() * this.scaling / 2);
        int centerY = (int) (Gdx.graphics.getHeight() * this.scaling / 2);
        coordinate.x = (short) (this.player.x - centerX + screenX * scaling);
        coordinate.y = (short) (this.player.y - centerY + (Gdx.graphics.getHeight() - screenY) * this.scaling);
        return coordinate;
    }

    public void moveTo(MovableEntity surface, short xMove, short yMove) {
        if (0 == xMove && 0 == yMove) {
            return;
        }

        final short oldStartX = surface.x(), oldStartY = surface.y();
        final short oldEndX = (short) (oldStartX + surface.texture().getRegionWidth() - 1);
        final short oldEndY = (short) (oldStartY + surface.texture().getRegionHeight() - 1);

        Array<Coordinate> toOccupiedCoordinates = new Array<>();
        Array<Coordinate> toReleasedCoordinates = new Array<>();
        Point targetPoint;
        Coordinate targetCoordinate;
        if (0 == xMove) {
            short occupiedY, releasedY;
            if (yMove > 0) {
                occupiedY = (short) (oldEndY + 1);
                releasedY = oldStartY;
            } else {
                occupiedY = (short) (oldStartY - 1);
                releasedY = oldEndY;
            }
            for (short x = oldStartX; x <= oldEndX; x++) {
                targetCoordinate = new Coordinate(x, occupiedY);
                if (targetCoordinate.x >= points.length || targetCoordinate.y >= points[0].length) {
                    return;
                }
                targetPoint = points[targetCoordinate.x][targetCoordinate.y];
                if (null == targetPoint || (null != targetPoint && null != targetPoint.obstruction && targetPoint.obstruction.type.isObstruction)) {
                    return;
                } else {
                    toOccupiedCoordinates.add(targetCoordinate);
                    toReleasedCoordinates.add(new Coordinate(x, releasedY));
                }
            }
        } else if (0 == yMove) {
            short occupiedX, releasedX;
            if (xMove > 0) {
                occupiedX = (short) (oldEndX + 1);
                releasedX = oldStartX;
            } else {
                occupiedX = (short) (oldStartX - 1);
                releasedX = oldEndX;
            }
            for (short y = oldStartY; y <= oldEndY; y++) {
                targetCoordinate = new Coordinate(occupiedX, y);
                if (targetCoordinate.x >= points.length || targetCoordinate.y >= points[0].length) {
                    return;
                }
                targetPoint = points[targetCoordinate.x][targetCoordinate.y];
                if (null == targetPoint || (null != targetPoint && null != targetPoint.obstruction && targetPoint.obstruction.type.isObstruction)) {
                    return;
                } else {
                    toOccupiedCoordinates.add(targetCoordinate);
                    toReleasedCoordinates.add(new Coordinate(releasedX, y));
                }
            }
        } else {
            short occupiedX, releasedX, occupiedY, releasedY, newStartX, newStartY, newEndX, newEndY;
            if (xMove > 0) {
                occupiedX = (short) (oldEndX + 1);
                releasedX = oldStartX;

                newStartX = (short) (releasedX + 1);
                newEndX = occupiedX;
            } else {
                occupiedX = (short) (oldStartX - 1);
                releasedX = oldEndX;

                newStartX = occupiedX;
                newEndX = (short) (releasedX - 1);
            }

            if (yMove > 0) {
                occupiedY = (short) (oldEndY + 1);
                releasedY = oldStartY;

                newStartY = (short) (releasedY + 1);
                newEndY = occupiedY;
            } else {
                occupiedY = (short) (oldStartY - 1);
                releasedY = oldEndY;

                newStartY = occupiedY;
                newEndY = (short) (releasedY - 1);
            }

            if (occupiedX >= points.length || releasedY >= points[0].length || releasedX >= points.length || occupiedY >= points[0].length) {
                return;
            }
            targetPoint = points[occupiedX][releasedY];
            if (null == targetPoint || (null != targetPoint && null != targetPoint.obstruction && targetPoint.obstruction.type.isObstruction)) {
                return;
            }
            targetPoint = points[releasedX][occupiedY];
            if (null == targetPoint || (null != targetPoint && null != targetPoint.obstruction && targetPoint.obstruction.type.isObstruction)) {
                return;
            }
            targetPoint = points[occupiedX][occupiedY];
            if (null == targetPoint || (null != targetPoint && null != targetPoint.obstruction && targetPoint.obstruction.type.isObstruction)) {
                return;
            } else {
                toOccupiedCoordinates.add(new Coordinate(occupiedX, occupiedY));
            }
            toReleasedCoordinates.add(new Coordinate(releasedX, releasedY));
            for (short x = newStartX; x <= newEndX; x++) {
                targetCoordinate = new Coordinate(x, occupiedY);
                if (targetCoordinate.x >= points.length || targetCoordinate.y >= points[0].length) {
                    return;
                }
                targetPoint = points[targetCoordinate.x][targetCoordinate.y];
                if (null == targetPoint || (null != targetPoint && null != targetPoint.obstruction && targetPoint.obstruction.type.isObstruction)) {
                    return;
                } else {
                    toOccupiedCoordinates.add(targetCoordinate);
                    toReleasedCoordinates.add(new Coordinate(x, releasedY));
                }
            }
            for (short y = newStartY; y <= newEndY; y++) {
                targetCoordinate = new Coordinate(occupiedX, y);
                if (targetCoordinate.x >= points.length || targetCoordinate.y >= points[0].length) {
                    return;
                }
                targetPoint = points[targetCoordinate.x][targetCoordinate.y];
                if (null == targetPoint || (null != targetPoint && null != targetPoint.obstruction && targetPoint.obstruction.type.isObstruction)) {
                    return;
                } else {
                    toOccupiedCoordinates.add(targetCoordinate);
                    toReleasedCoordinates.add(new Coordinate(releasedX, y));
                }
            }
        }

        toOccupiedCoordinates.forEach(coordinate -> {
            if (null != this.points[coordinate.x][coordinate.y]) {
                this.points[coordinate.x][coordinate.y].obstruction = surface;
            }
        });
        toReleasedCoordinates.forEach(coordinate -> {
            if (null != this.points[coordinate.x][coordinate.y]) {
                this.points[coordinate.x][coordinate.y].obstruction = null;
            }
        });
        toOccupiedCoordinates.clear();
        toReleasedCoordinates.clear();

        surface.x += xMove;
        surface.y += yMove;
    }

    public boolean build(Surface surface) {
        if (availableToBuild(surface)) {
            if (surface instanceof StaticEntity) {
                registerToMap((StaticEntity) surface);
            } else if (surface instanceof MovableEntity) {
                registerToMap((MovableEntity) surface);
            }
            surfaces.add(surface);

            if (null != surface.type.buildRecipeItems) {
                for (RecipeItem recipeItem : surface.type.buildRecipeItems) {
                    switch (recipeItem.resourceType) {
                        case Stone: {
                            Global.world.resourceWarehouse.stoneCount -= recipeItem.amount;
                            break;
                        }
                    }
                }
            }

            return true;
        } else {
            return false;
        }
    }

    public boolean fight(Bullet bullet) {
        short startX = (short) MathUtils.floor(bullet.x), endX = (short) MathUtils.ceil(bullet.x);
        short startY = (short) MathUtils.floor(bullet.y), endY = (short) MathUtils.ceil(bullet.y);

        Point point = this.points[startX][startY];
        if (fight(point, bullet)) {
            return true;
        }
        point = this.points[startX][endY];
        if (fight(point, bullet)) {
            return true;
        }
        point = this.points[endX][startY];
        if (fight(point, bullet)) {
            return true;
        }
        point = this.points[endX][endY];
        if (fight(point, bullet)) {
            return true;
        }

        return false;
    }

    private boolean fight(Point point, Bullet bullet) {
        if (null != point) {
            if (null != point.obstruction && point.obstruction.league != bullet.from) {
                point.obstruction.hp -= bullet.type.damage;
                return true;
            }
            if (null != point.surface && point.surface.league != bullet.from) {
                point.surface.hp -= bullet.type.damage;
                return true;
            }
        }
        return false;
    }

    public boolean availableToBuild(TextureUnit textureUnit) {
        short startX = textureUnit.x();
        short startY = textureUnit.y();
        short endX = (short) (startX + textureUnit.texture().getRegionWidth());
        short endY = (short) (startY + textureUnit.texture().getRegionHeight());

        for (short x = startX; x < endX; x++) {
            for (short y = startY; y < endY; y++) {
                if (null != this.points[x][y] && (null != this.points[x][y].surface || null != this.points[x][y].obstruction)) {
                    return false;
                }
            }
        }

        return true;
    }

    private void bindCoordinate(TextureUnit unit, Consumer<Point> consumer) {
        short startX = unit.x(), endX = (short) (unit.x() + unit.texture().getRegionWidth());
        short startY = unit.y(), endY = (short) (unit.y() + unit.texture().getRegionHeight());
        for (short x = startX; x < endX; x++) {
            for (short y = startY; y < endY; y++) {
                if (null == points[x][y]) {
                    points[x][y] = new Point();
                }
                Point point = points[x][y];
                consumer.accept(point);
            }
        }
    }

    private void registerToMap(MovableEntity surface) {
        bindCoordinate(surface, point -> {
            if (surface.type.isObstruction) {
                point.obstruction = surface;
            } else {
                point.surface = surface;
            }
        });
    }

    private void registerToMap(StaticEntity surface) {
        AtomicReference<Float> productivity = new AtomicReference<>(0f);
        AtomicReference<GroundType> resourceType = new AtomicReference<>();
        bindCoordinate(surface, point -> {
            if (surface.type.isObstruction) {
                point.obstruction = surface;
            } else {
                point.surface = surface;
            }
            if (null != point.ground) {
                productivity.set(productivity.get() + point.ground.type.productivity / 64);
                resourceType.set(point.ground.type);
            }
        });

        if (surface instanceof StaticEntity) {
            link(surface);

            if (surface instanceof Drill) {
                ((Drill) surface).productivity = productivity.get();
                switch (resourceType.get()) {
                    case Stone: {
                        ((Drill) surface).resourceType = ResourceType.Stone;
                        break;
                    }
                }
            }
        }
    }

    private void link(StaticEntity unit) {
        short startX = unit.x(), endX = (short) (unit.x() + unit.texture().getRegionWidth());
        short startY = unit.y(), endY = (short) (unit.y() + unit.texture().getRegionHeight());

        Point up = this.points[startX][endY];
        if (null != up) {
            if (null != up.surface) {
                if (up.surface instanceof StaticEntity) {
                    unit.tryJoin(Gate.UP, Gate.BOTTOM, (StaticEntity) up.surface);
                }
            } else if (null != up.obstruction) {
                if (up.obstruction instanceof StaticEntity) {
                    unit.tryJoin(Gate.UP, Gate.BOTTOM, (StaticEntity) up.obstruction);
                }
            }
        }
        Point right = this.points[endX][startY];
        if (null != right) {
            if (null != right.surface) {
                if (right.surface instanceof StaticEntity) {
                    unit.tryJoin(Gate.RIGHT, Gate.LEFT, (StaticEntity) right.surface);
                }
            } else if (null != right.obstruction) {
                if (right.obstruction instanceof StaticEntity) {
                    unit.tryJoin(Gate.RIGHT, Gate.LEFT, (StaticEntity) right.obstruction);
                }
            }
        }
        Point bottom = this.points[startX][startY - 1];
        if (null != bottom) {
            if (null != bottom.surface) {
                if (bottom.surface instanceof StaticEntity) {
                    unit.tryJoin(Gate.BOTTOM, Gate.UP, (StaticEntity) bottom.surface);
                }
            } else if (null != bottom.obstruction) {
                if (bottom.obstruction instanceof StaticEntity) {
                    unit.tryJoin(Gate.BOTTOM, Gate.UP, (StaticEntity) bottom.obstruction);
                }
            }
        }
        Point left = this.points[startX - 1][startY];
        if (null != left) {
            if (null != left.surface) {
                if (left.surface instanceof StaticEntity) {
                    unit.tryJoin(Gate.LEFT, Gate.RIGHT, (StaticEntity) left.surface);
                }
            } else if (null != left.obstruction) {
                if (left.obstruction instanceof StaticEntity) {
                    unit.tryJoin(Gate.LEFT, Gate.RIGHT, (StaticEntity) left.obstruction);
                }
            }
        }
    }

    private void registerToMap(Ground ground) {
        bindCoordinate(ground, point -> {
            point.ground = ground;
        });
    }

    private void setupMap(Pixmap pixmap) {
        int width = pixmap.getWidth(), height = pixmap.getHeight();
        final int COLOR_BLACK = Color.rgba8888(Color.BLACK);
        final int COLOR_WHITE = Color.rgba8888(Color.WHITE);
        final int COLOR_RED = Color.rgba8888(Color.RED);
        final int COLOR_BLUE = Color.rgba8888(Color.BLUE);
        for (short x = 0; x < width; x++) {
            for (short y = 0; y < height; y++) {
                int color = pixmap.getPixel(x, y);
                if (COLOR_BLACK == color) {
                    Ground ground = new Ground();
                    switch (MathUtils.random(4)) {
                        case 0: {
                            ground.type = GroundType.Coal;
                            break;
                        }
                        case 1: {
                            ground.type = GroundType.Grass;
                            break;
                        }
                        case 2: {
                            ground.type = GroundType.Dirt;
                            break;
                        }
                        case 3: {
                            ground.type = GroundType.Iron;
                            break;
                        }
                        default: {
                            ground.type = GroundType.Stone;
                            break;

                        }
                    }

                    ground.x = (short) (x * 8);
                    ground.y = (short) (y * 8);

                    grounds.add(ground);
                    registerToMap(ground);
                } else if (COLOR_WHITE == color) {
                    StaticEntity surface = new StaticEntity();
                    surface.type = SurfaceType.StoneWall;
                    surface.x = (short) (x * 8);
                    surface.y = (short) (y * 8);

                    surfaces.add(surface);
                } else if (COLOR_BLUE == color) {
                    base = new Base();
                    base.x = (short) (x * 8);
                    base.y = (short) (y * 8);

                    registerToMap(base);
                } else if (582044927 == color) {
                    player = new Player();
                    player.x = (short) (x * 8);
                    player.y = (short) (y * 8);

                    registerToMap(player);

                    Ground ground = new Ground();
                    ground.type = GroundType.Stone;
                    ground.x = (short) (x * 8);
                    ground.y = (short) (y * 8);

                    grounds.add(ground);
                    registerToMap(ground);
                } else if (-1010580481 == color) {
                    Ground ground = new Ground();
                    ground.type = GroundType.Stone;
                    ground.x = (short) (x * 8);
                    ground.y = (short) (y * 8);

                    grounds.add(ground);
                    registerToMap(ground);
                } else {
                }
            }
        }
    }

    private void bindTextures(TextureAtlas textureAtlas) {
        GroundType.Coal.texture = textureAtlas.findRegion(GroundType.Coal.textureName);
        GroundType.Grass.texture = textureAtlas.findRegion(GroundType.Grass.textureName);
        GroundType.Dirt.texture = textureAtlas.findRegion(GroundType.Dirt.textureName);
        GroundType.Iron.texture = textureAtlas.findRegion(GroundType.Iron.textureName);
        GroundType.Stone.texture = textureAtlas.findRegion(GroundType.Stone.textureName);
        SurfaceType.StoneWall.texture = textureAtlas.findRegion(SurfaceType.StoneWall.textureName);
        SurfaceType.Base.texture = textureAtlas.findRegion(SurfaceType.Base.textureName);
        SurfaceType.Player.texture = textureAtlas.findRegion(SurfaceType.Player.textureName);
        SurfaceType.Mech.texture = textureAtlas.findRegion(SurfaceType.Mech.textureName);
        BulletType.Stone.texture = textureAtlas.findRegion("bullet");
    }

    @Override
    public void dispose() {
    }
}
